﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml.Schema;
using System.Xml;
using System.Xml.Linq;
using System.Reflection;

namespace SpringNetCheckerCore
{
    /// <summary>
    /// Spring.NET Objects Checker
    /// </summary>
    public class SpringNetObjectsChecker : ISpringNetObjectsChecker
    {
        /// <summary>
        /// 開放寫Log訊息功能事件給使用端
        /// </summary>
        public event LogWriteLineDelegate OnLogWriteLine;

        /// <summary>
        /// Spring Objects XML Schema 定義檔的位置
        /// </summary>
        public string SpringObjectsSchemaFilePath { get; set; }

        /// <summary>
        /// Spring Database XML Schema 定義檔的位置
        /// </summary>
        public string SpringDatabaseSchemaFilePath { get; set; }

        /// <summary>
        /// 總錯誤數量
        /// </summary>
        private int errorCountTotal = 0;

        /// <summary>
        /// 總警告數量
        /// </summary>
        private int warningCountTotal = 0;

        /// <summary>
        /// 物件設定檔的xml格式是否正確
        /// </summary>
        private bool isXmlSchemaPassed = true;

        /// <summary>
        /// 組件資料清單
        /// </summary>
        private IList<AssemblyData> _assemblyDatas = new List<AssemblyData>();

        /// <summary>
        /// 檢查每個物件型別時的來源對象
        /// </summary>
        private enum CheckObjectsTypeSourceEnum
        {
            None = 0,
            Object,
            Property
        };

        /// <summary>
        /// 記錄訊息的嚴重程度
        /// </summary>
        private enum LogSeverityEnum
        {
            Information=0,
            Warning,
            Error
        };

        /// <summary>
        /// 檢查完每個物件型別後的結果
        /// </summary>
        private class CheckObjectsTypeResult
        {
            public bool isEverythingPassed = true; //檢查結果
            public int ErrorCount = 0;
            public int WarningCount = 0;
        }

        /// <summary>
        /// 檢查完每個物件參考後的結果
        /// </summary>
        private class CheckObjectsReferenceResult
        {
            public bool isEverythingPassed = true; //檢查結果
            public int ErrorCount = 0;
        }

        /// <summary>
        /// Spring.NET Objects Checker
        /// </summary>
        public SpringNetObjectsChecker(string springObjectsSchemaFilePath, string springDatabaseSchemaFilePath)
        {
            //設定 Spring Objects XML Schema 定義檔的位置
            SpringObjectsSchemaFilePath = springObjectsSchemaFilePath;
            SpringDatabaseSchemaFilePath = springDatabaseSchemaFilePath;
        }

        /// <summary>
        /// 新增寫Log訊息的委派
        /// </summary>
        /// <param name="logWriteLine"></param>
        public void AddLogWriteLineDelegate(LogWriteLineDelegate logWriteLine)
        {
            OnLogWriteLine += logWriteLine;
        }

        /// <summary>
        /// 檢查 Spring Objects Xml 設定檔
        /// </summary>
        /// <param name="objectsXmlFilePath">Spring Objects Xml 設定檔位置</param>
        /// <param name="assembliesDirectoryPath">使用到的組件檔資料夾位置</param>
        /// <returns></returns>
        public bool CheckObjectsXmlFile(string objectsXmlFilePath, string assembliesDirectoryPath)
        {
            //重設錯誤數量
            errorCountTotal = 0;
            warningCountTotal = 0;

            LogWriteLine(string.Format("Current Directory[{0}]", Environment.CurrentDirectory));
            LogWriteLine(string.Format("CheckObjectsXmlFile([{0}], [{1}])", objectsXmlFilePath, assembliesDirectoryPath));

            //檢查物件設定檔和組件資料夾是否存在
            LogWriteLine("Checking Parameters ...");
            FileInfo objectsXmlFileInfo = new FileInfo(objectsXmlFilePath);
            if (!objectsXmlFileInfo.Exists)
            {
                LogWriteLine(string.Format("File[{0}] is nonexistent.", objectsXmlFilePath), LogSeverityEnum.Error);

                //加至總計
                errorCountTotal += 1;

                //顯示錯誤總計
                ShowTotalErrorCount();

                return false;
            }

            DirectoryInfo assembliesDirectoryInfo = new DirectoryInfo(assembliesDirectoryPath);
            if (!assembliesDirectoryInfo.Exists)
            {
                LogWriteLine(string.Format("Directory[{0}] is not nonexistent.", objectsXmlFilePath), LogSeverityEnum.Error);

                //加至總計
                errorCountTotal += 1;

                //顯示錯誤總計
                ShowTotalErrorCount();

                return false;
            }

            LogWriteLine("Parameters passed.");

            //檢查物件設定檔的xml格式是否正確
            LogWriteLine("Checking XML Schema ...");
            XmlSchemaSet springObjectsXsd = new XmlSchemaSet();
            springObjectsXsd.Add("http://www.springframework.net", SpringObjectsSchemaFilePath);
            springObjectsXsd.Add("http://www.springframework.net/database", SpringDatabaseSchemaFilePath);

            XDocument objectsXmlDoc = null;
            try
            {
                objectsXmlDoc = XDocument.Load(objectsXmlFilePath);
            }
            catch (Exception ex)
            {
                //載入物件設定檔時發生例外錯誤
                isXmlSchemaPassed = false;
                LogWriteLine(string.Format("Exception error occurred while loading XML file: {0}", ex.Message), LogSeverityEnum.Error);

                //加至總計
                errorCountTotal += 1;

                //顯示錯誤總計
                ShowTotalErrorCount();

                return false;
            }
            objectsXmlDoc.Validate(springObjectsXsd, new ValidationEventHandler(SpringObjectsValidationEventHandler));

            if (!isXmlSchemaPassed)
            {
                //顯示錯誤總計
                ShowTotalErrorCount();

                return false;
            }

            LogWriteLine("XML Schema passed.");

            //檢查物件設定檔中指定的物件類別是否存在
            LogWriteLine("Checking declared objects ...");

            //讀取每個object的資訊(id,type,property(name,value,ref))

            //檢查objects的id是否有重覆的(Schema 驗證時會擋掉,這裡應該用不到了)
            var duplicateObjects = from item in objectsXmlDoc.Root.Elements()
                                   where item.Name.LocalName == "object"
                                    && item.Attribute("id") != null
                                   group item by item.Attribute("id").Value into itemGroup
                                   where itemGroup.Count() > 1
                                   select itemGroup;

            if (duplicateObjects.Count() > 0)
            {
                foreach (var dupObj in duplicateObjects)
                {
                    LogWriteLine(string.Format("There are {0} of duplicate objects id[{1}].", dupObj.Count(), dupObj.Key), LogSeverityEnum.Error);

                    //加至總計
                    errorCountTotal += 1;
                }
            }

            var objects = from item in objectsXmlDoc.Root.Elements()
                          where item.Name.LocalName == "object"
                          select item;

            var valueProperties = from item in objects.Elements()
                                  where item.Attribute("ref") == null
                                    || item.Attribute("value") != null  //萬一有 ref 也有 value,也檢查
                                  select item;

            //整理出所有object和property會用到的組件
            //檢查每個object的type指定類別在組件中是否存在
            CheckObjectsTypeResult objectsResult = CheckObjectsType(assembliesDirectoryPath, CheckObjectsTypeSourceEnum.Object, objects);
            LogWriteLine(string.Format("There are {0} Errors and {1} Warnings.", objectsResult.ErrorCount, objectsResult.WarningCount));

            //加至總計
            errorCountTotal += objectsResult.ErrorCount;
            warningCountTotal += objectsResult.WarningCount;

            if (objectsResult.isEverythingPassed)
            {
                //沒問題
                LogWriteLine("All objects passed.");
            }

            //檢查property的value指定類別在組件中是否存在
            LogWriteLine("Checking properties of objects ...");
            CheckObjectsTypeResult propertiesResult = CheckObjectsType(assembliesDirectoryPath, CheckObjectsTypeSourceEnum.Property, valueProperties);
            LogWriteLine(string.Format("There are {0} Errors and {1} Warnings.", propertiesResult.ErrorCount, propertiesResult.WarningCount));

            //加至總計
            errorCountTotal += propertiesResult.ErrorCount;
            warningCountTotal += propertiesResult.WarningCount;

            if (propertiesResult.isEverythingPassed)
            {
                //沒問題
                LogWriteLine("All properties of objects passed.");
            }

            //檢查object的parent指定的id在object清單中是否存在
            LogWriteLine("Checking the objects that are parent of other objects ...");

            var hasParentObjects = from item in objects
                                   where item.Attribute("parent") != null
                                   select item;

            CheckObjectsReferenceResult objRefResult = CheckObjectsReference(
                objectsXmlDoc, CheckObjectsTypeSourceEnum.Object, hasParentObjects);
            LogWriteLine(string.Format("There are {0} Errors.", objRefResult.ErrorCount));

            //加至總計
            errorCountTotal += objRefResult.ErrorCount;

            if (objRefResult.isEverythingPassed)
            {
                //沒問題
                LogWriteLine("All objects referred existent parent object.");
            }

            //檢查property的ref指定的id在object清單中是否存在,也可能在<db:provider>中
            LogWriteLine("Checking the objects that are referred by properties ...");
            CheckObjectsReferenceResult propRefResult = CheckObjectsReference(
                objectsXmlDoc, CheckObjectsTypeSourceEnum.Property, objects.Elements());
            LogWriteLine(string.Format("There are {0} Errors.", propRefResult.ErrorCount));

            //加至總計
            errorCountTotal += propRefResult.ErrorCount;

            if (propRefResult.isEverythingPassed)
            {
                //沒問題
                LogWriteLine("All properties referred existent object.");
            }

            //顯示錯誤總計
            ShowTotalErrorCount();

            return true;
        }

        /// <summary>
        /// 顯示錯誤總計
        /// </summary>
        private void ShowTotalErrorCount()
        {
            LogWriteLine(string.Format("Total {0} Errors and {1} Warnings.", errorCountTotal, warningCountTotal));
        }

        /// <summary>
        /// 檢查每個物件的型別
        /// </summary>
        private CheckObjectsTypeResult CheckObjectsType(string assembliesDirectoryPath, CheckObjectsTypeSourceEnum sourceMode, IEnumerable<XElement> elements)
        {
            //預設檢查結果
            CheckObjectsTypeResult checkResult = new CheckObjectsTypeResult()
            {
                 isEverythingPassed = true
            };

            string idAttrName = "id";   //識別碼屬性名稱
            string typeAttrName = "type";   //型別屬性名稱

            if (sourceMode == CheckObjectsTypeSourceEnum.Property)
            {
                idAttrName = "name";
                typeAttrName = "value";
            }

            foreach (XElement obj in elements)
            {
                //跳過不知道怎麼檢查的物件,value值裡沒有逗號的項目(也許是設定字串值)
                if (obj.Attribute(typeAttrName) == null
                    || obj.Attribute(typeAttrName).Value.IndexOf(',') < 0)
                {
                    //如果是含parent的object,跳過不需記錄,parent會另外查
                    if (sourceMode == CheckObjectsTypeSourceEnum.Object
                        && obj.Attribute("parent") != null)
                    {
                        continue;
                    }

                    //只列出開頭的標籤內容
                    int endOfTagHead = obj.ToString().IndexOf('>') + 1;
                    string tagHead = obj.ToString().Substring(0, endOfTagHead);

                    LogWriteLine(string.Format("Skip to check {0}[{1}]", sourceMode, tagHead), LogSeverityEnum.Warning);
                    checkResult.WarningCount += 1;
                    continue;
                }

                //取得組件名稱
                string[] objTypeContent = obj.Attribute(typeAttrName).Value.Split(',');
                string classFullName = objTypeContent[0].Trim();
                string assemblyName = objTypeContent[1].Trim();

                //檢查組件資料是否載入過
                var asmResults = _assemblyDatas.Where(x => string.Compare(x.Name, assemblyName, true) == 0);

                AssemblyData asmData = null;

                if (asmResults.Count() == 0)
                {
                    //試著讀取組件資訊
                    bool asmIsExistent = false;
                    Assembly asm = null;
                    string asmFileName = string.Format(@"{0}\{1}.dll", assembliesDirectoryPath, assemblyName);
                    try
                    {
                        asm = Assembly.LoadFrom(asmFileName);
                        asmIsExistent = true;
                    }
                    catch (Exception ex)
                    {
                        LogWriteLine(string.Format("Exception error occurred while loading assembly file[{0}]: {1}",
                            asmFileName, ex.Message), LogSeverityEnum.Error);

                        checkResult.ErrorCount += 1;
                        checkResult.isEverythingPassed = false;
                    }

                    //暫存組件資料
                    asmData = new AssemblyData()
                    {
                        Name = assemblyName,
                        IsExistent = asmIsExistent,
                        LoadedAssembly = asm
                    };

                    _assemblyDatas.Add(asmData);
                }
                else
                {
                    //取得暫存的組件資料
                    asmData = asmResults.First();
                }

                //取得id,不是每個項目都會指定id
                string idValue = "";
                if (obj.Attribute(idAttrName) != null)
                    idValue = obj.Attribute(idAttrName).Value;

                if (asmData.IsExistent)
                {
                    //檢查類別,Spring.NET 不區分大小寫
                    if (sourceMode == CheckObjectsTypeSourceEnum.Object)
                    {
                        asmData.RelativeObjects.Add(obj);
                    }
                    else if (sourceMode == CheckObjectsTypeSourceEnum.Property)
                    {
                        asmData.RelativeProperties.Add(obj);
                    }

                    //先檢查這個類別有沒有被找過
                    var classResults = asmData.ClassIsExistent.Keys.Where(x => string.Compare(x, classFullName, true) == 0);
                    bool classIsExistent = false;
                    if (classResults.Count() == 0)
                    {
                        //試著讀取類別資料
                        var classDatas = from typeItem in asmData.LoadedAssembly.GetTypes()
                                         where string.Compare(typeItem.FullName, classFullName, true) == 0
                                         select typeItem;

                        //類別搜尋結果
                        classIsExistent = (classDatas.Count() > 0) ? true : false;

                        //存下類別搜尋結果
                        asmData.ClassIsExistent.Add(classFullName, classIsExistent);
                    }
                    else
                    {
                        //取得暫存的類別搜尋結果
                        string key = classResults.ElementAt(0);
                        classIsExistent = asmData.ClassIsExistent[key];
                    }

                    if (!classIsExistent)
                    {
                        LogWriteLine(string.Format("{0}-{1}[{2}] loading class[{3}] from assembly[{4}] failed!",
                            sourceMode, idAttrName, idValue, classFullName, asmData.Name), LogSeverityEnum.Error);

                        checkResult.ErrorCount += 1;
                        checkResult.isEverythingPassed = false;
                    }
                }
                else
                {
                    //物件沒有組件資料可用
                    LogWriteLine(string.Format("{0}-{1}[{2}] loading assembly[{3}] failed!",
                        sourceMode, idAttrName, idValue, asmData.Name), LogSeverityEnum.Error);

                    checkResult.ErrorCount += 1;
                    checkResult.isEverythingPassed = false;
                }
            }

            return checkResult;
        }

        /// <summary>
        /// 檢查每個物件的參考
        /// </summary>
        private CheckObjectsReferenceResult CheckObjectsReference(
            XDocument objectsXmlDoc, CheckObjectsTypeSourceEnum sourceMode, IEnumerable<XElement> sourceElements)
        {
            //預設檢查結果
            CheckObjectsReferenceResult checkResult = new CheckObjectsReferenceResult()
            {
                isEverythingPassed = true
            };

            string idAttrName = "id";   //識別碼屬性名稱
            string refAttrName = "parent"; //參考來源屬性名稱

            if (sourceMode == CheckObjectsTypeSourceEnum.Property)
            {
                idAttrName = "name";
                refAttrName = "ref";
            }

            //檢查object.parent, property.ref指定的id在object清單中是否存在,property.ref也可能在<db:provider>中
            var emptyObjItems = from item in
                                    (from item in sourceElements
                                     where item.Attribute(refAttrName) != null
                                     select item)  //object.parent, property.ref
                                join obj in
                                    (from obj in objectsXmlDoc.Root.Elements()
                                     where obj.Attribute("id") != null
                                     select obj)  //所有有id的項目,不能只有object
                                on item.Attribute(refAttrName).Value equals obj.Attribute("id").Value into itemObjects   //join後的內容只有objects
                                from leftJoinItems in itemObjects.DefaultIfEmpty()  //do left join
                                where leftJoinItems == null
                                select new
                                {
                                    parentObjectIdAttr = item.Parent.Attribute("id"),
                                    idAttr = item.Attribute(idAttrName),
                                    refAttr = item.Attribute(refAttrName)
                                };

            if (emptyObjItems.Count() > 0)
            {
                //發現沒對到的物件
                checkResult.isEverythingPassed = false;

                foreach (var emptyObjItem in emptyObjItems)
                {
                    string parentIdValue = "";
                    if (emptyObjItem.parentObjectIdAttr != null)
                        parentIdValue = emptyObjItem.parentObjectIdAttr.Value;

                    string idValue = "";
                    if (emptyObjItem.idAttr != null)
                        idValue = emptyObjItem.idAttr.Value;

                    switch(sourceMode)
                    {
                        case CheckObjectsTypeSourceEnum.Object:
                            LogWriteLine(string.Format("Object-id[{0}] refferred nonexistent parent object-id[{1}]",
                                idValue, emptyObjItem.refAttr.Value), LogSeverityEnum.Error);
                            break;
                        case CheckObjectsTypeSourceEnum.Property:
                            LogWriteLine(string.Format("Property-name[{0}] of object-id[{1}] refferred nonexistent object-id[{2}]",
                                idValue, parentIdValue, emptyObjItem.refAttr.Value), LogSeverityEnum.Error);
                            break;
                    }

                    checkResult.ErrorCount += 1;
                }
            }

            return checkResult;
        }

        private void SpringObjectsValidationEventHandler(object sender, ValidationEventArgs e)
        {
            //檢查物件設定檔的xml格式時發生錯誤
            isXmlSchemaPassed = false;

            switch (e.Severity)
            {
                case XmlSeverityType.Error:
                    LogWriteLine(string.Format("Error occurred while checking XML Schema: {0}", e.Message), LogSeverityEnum.Error);

                    //加至總計
                    errorCountTotal += 1;
                    break;
                case XmlSeverityType.Warning:
                    LogWriteLine(string.Format("Warning occurred while checking XML Schema: {0}", e.Message), LogSeverityEnum.Error);

                    //加至總計
                    warningCountTotal += 1;
                    break;
            }

        }

        /// <summary>
        /// 寫Log訊息
        /// </summary>
        private void LogWriteLine(string value)
        {
            LogWriteLine(value, LogSeverityEnum.Information);
        }

        /// <summary>
        /// 寫Log訊息
        /// </summary>
        private void LogWriteLine(string value, LogSeverityEnum logSeverity)
        {
            if (OnLogWriteLine == null)
                return;

            OnLogWriteLine(string.Concat(logSeverity, "!\t", value));
        }
    }
}
